Docker 安装常用软件说明

Tomcat

查询和拉取镜像

Tomcat 的 Docker Hub 下载地址,对于初学者来说,Tomcat 镜像的 Tag 命名看起来像一串乱码(例如 9.0.118-jdk17-corretto-al2),但其实它遵循了极其严谨的“组合式命名规范”。 这串名字是由 4 个核心部分拼接而成的,就像公式一样:[Tomcat版本]-[JDK版本]-[JDK发行版]-[底层操作系统]。如果是生产环境上线,强烈建议选择带有精确三位版本号的标签,例如 9.0.118-jdk17-corretto。这样可以保证无论何时自动化构建(CI/CD)拉取镜像,代码环境都保持绝对一致,避免因为 Tomcat 偷偷升级导致应用崩溃。

  • 第一部分:Tomcat 版本(核心中间件)。9.0.118 (具体三位版本号):最精确的生产级版本。推荐在生产环境使用,能死死锁住版本,防止意外升级。9.0 (两位版本号):动态指针,永远指向 9.0.x 系列中的最新版。9 (主版本号):动态指针,指向大版本 9.x.x 的最新版。
  • 第二部分:Java / JDK 版本(运行环境)。对应名字中的 jdk8、jdk11、jdk17、jdk21、jre25 等。jdk17 说明容器内安装的是 JDK 17(包含完整的 Java 开发工具包和编译器),jre25 说明内嵌的是 JRE 25(仅包含运行时环境,体积更小,安全性更高,因为去掉了编译工具)。
  • 第三部分:JDK 发行版(谁家提供的 Java)。对应名字中的 corretto、temurin 等,由于 Oracle JDK 商业化策略的调整,目前主流镜像都使用开源的 OpenJDK 衍生版。corretto (亚马逊提供),这是亚马逊官方维护的免费、多平台、生产就绪的 OpenJDK 发行版。它在云原生和 AWS 生态中性能极佳,长久稳定支持。temurin (Eclipse 基金会提供)Eclipse Temurin(原名 AdoptOpenJDK)。目前非常主流、极为纯净、完全开源的 Java 运行时,广泛用于各大企业的标准环境。
  • 第四部分:底层操作系统(洋葱模型的底座)。对应名字末尾的 al2、noble,或者有些省略不写的。-al2 (Amazon Linux 2) 说明这个镜像的最底层操作系统是亚马逊基于 RHEL(红帽)定制的 Amazon Linux 2 操作系统(在使用习惯上类似于 CentOS/RHEL)。-noble (Ubuntu 24.04) 说明底层操作系统是 Ubuntu 24.04 LTS (代号 Noble Numbat)。如果名字到 corretto 就戛然而止了(例如 9.0-jdk17-corretto),它默认通常会使用标准的 Debian 或标准的 Linux 基础层。
1
$ docker pull tomcat:9.0.118-jdk17-corretto


创建容器实例

1
2
3
4
5
6
7
8
9
10
11
12
13
# 启动一个tomcat实例
$ docker run -d -p 8080:8080 --name=tomcat01 tomcat:9.0.118-jdk17-corretto
# 进入到docker实例内部
$ docker -it 8d5d3164637e /bib/bash

# 更改文件夹
$$ cd /usr/local/tomcat
$$ ls -a # 可以观察到 webapps 和 webapps.dist 等文件夹,真正有货的是 webapps.dist
$$ rm -rf webapps
$$ mv webapps.dist webapps

# 访问
curl http://192.168.1.8:8080


MySQL

基础安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 查询和拉取镜像
$ docker pull mysql:8.4.9-oraclelinux9

# 启动实例(注意一定要添加数据卷,防止数据丢失!)
$ docker run \
-p 3306:3306 \
--privileged=true \
-v /opt/apps/mysql/data:/var/lib/mysql \
-v /opt/apps/mysql/conf:/etc/mysql/conf.d \
-v /opt/apps/mysql/log:/var/log \
--name=mysql01 \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.4.9-oraclelinux9 \
--mysql-native-password=ON
$ docker ps

# 进入到 docker 容器内部
$ docker exec -it 51bc3f102318 /bin/bash
$$ mysql -uroot -p

# 初步测试
$$$ show databases;
$$$ create database db01;
$$$ use db01;
$$$ create table t1(id int not null primary key auto_increment, name varchar(20));
$$$ insert into t1(id, name) values(1, '张三');

# 查看当前的用户和加密方式(你会发现 root 这一行确实是 caching_sha2_password)
$$$ SELECT user, host, plugin FROM mysql.user;
# 将 root 用户的加密方式降级改为老版本兼容模式,两步走完,输入 exit 退出。现在重新去刷新你的老版本 Navicat,就可连到mysql。
$$$ ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';

更改编码设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# 查看默认编码集
$$$ show variables like 'character%';
+--------------------------+--------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------+
| character_set_client | latin1 |
| character_set_connection | latin1 |
| character_set_database | utf8mb4 | #### 数据库的编码
| character_set_filesystem | binary |
| character_set_results | latin1 |
| character_set_server | utf8mb4 |
| character_set_system | utf8mb3 |
| character_sets_dir | /usr/share/mysql-8.4/charsets/ |
+--------------------------+--------------------------------+

# 在宿主机修改配置文件
$ /opt/apps/mysql/conf$ ls
$ /opt/apps/mysql/conf$ vim my.cnf
[client]
default_character_set=utf8
[mysqld]
collation_server=utf8_general_ci
character_set_server=utf8

# 重启mysql实例
$ docker restart mysql01

# 再次进入mysql docker容器,查看编码
$$$ show variables like 'character%';
+--------------------------+--------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------+
| character_set_client | utf8mb3 |
| character_set_connection | utf8mb3 |
| character_set_database | utf8mb3 |
| character_set_filesystem | binary |
| character_set_results | utf8mb3 |
| character_set_server | utf8mb3 |
| character_set_system | utf8mb3 |
| character_sets_dir | /usr/share/mysql-8.4/charsets/ |
+--------------------------+--------------------------------+

# 验证
$$$ create database db02;
$$$ use db02;
$$$ create table t1(id int not null primary key auto_increment, name varchar(20));
$$$ insert into t1(id, name) values(1, '张三');
$$$ select * from t1 limit 0,10;


主从复制

更改主服务器配置。在宿主机编辑配置文件(master配置文件): /opt/apps/mysql/conf/my.cnf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[client]
default_character_set=utf8

[mysqld]
## 编码设置
collation_server=utf8_general_ci
character_set_server=utf8

## 设置 server_id,同一局域网中需要唯一
server_id=101

## 指定不需要同步的数据库名称
binlog-ignore-db=mysql

## 开启二进制日志功能
log-bin=mall-mysql-bin

## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M

## 设置使用的二进制日志格式(mixed, statement, row)
binlog_format=mixed

## 二进制日志过期清理时间。默认值为0,表示不自动清理。
# expire_logs_days=7
# MySQL 8.0 更推荐使用秒级控制的参数
binlog_expire_logs_seconds=604800

## 跳过主从复制中遇到的所有错误或指定类型的错误,避免 slave 端复制中断。
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062

重启master实例:

1
2
$ docker restart mysql01
$ docker ps

登录数据库,创建数据同步的用户

1
2
3
4
5
6
$ docker exec -it 51bc3f102318 /bin/bash
$$ mysql -uroot -p
$$$ show databases;

$$$ create user 'slave'@'%' identified by '123456';
$$$ grant replication slave, replication client on *.* to 'slave'@'%';

退出到宿主机,创建从服务器 3308

1
2
3
4
5
6
7
8
9
10
$ docker run \
-p 3308:3306 \
--privileged=true \
-v /opt/apps/mysql-slave/data:/var/lib/mysql \
-v /opt/apps/mysql-slave/conf:/etc/mysql/conf.d \
-v /opt/apps/mysql-slave/log:/var/log \
--name=mysql02 \
-e MYSQL_ROOT_PASSWORD=123456 \
-d mysql:8.4.9-oraclelinux9 \
--mysql-native-password=ON

编辑从服务器的配置文件 /opt/apps/mysql-slave/conf/my.cnf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[client]
default_character_set=utf8

[mysqld]
## 编码设置
collation_server=utf8_general_ci
character_set_server=utf8

## 设置 server_id,同一局域网中需要唯一
server_id=102

## 指定不需要同步的数据库名称
binlog-ignore-db=mysql

## 开启二进制日志功能,以备 Slave 作为其它数据库实例的 Master 时使用
log-bin=mall-mysql-slave1-bin

## 设置二进制日志使用内存大小(事务)
binlog_cache_size=1M

## 设置使用的二进制日志格式(mixed, statement, row)
binlog_format=mixed

## 二进制日志过期清理时间。默认值为0,表示不自动清理。
# 如果在 8.0+ 生产环境,建议替换为秒级控制
# expire_logs_days=7
binlog_expire_logs_seconds=604800

## 跳过主从复制中遇到的所有错误或指定类型的错误,避免 slave 端复制中断。
## 如:1062错误是指一些主键重复,1032错误是因为主从数据库数据不一致
slave_skip_errors=1062

## relay_log 配置中继日志
relay_log=mall-mysql-relay-bin

## log_slave_updates 表示 slave 将复制事件写进自己的二进制日志
## 8.0.26 及以上版本,这个参数已经被废弃并改名。新版推荐使用 log_replica_updates=1,它们的效果完全一致。
# log_slave_updates=1
log_replica_updates=1

## slave 设置为只读(具有 super 权限的用户除外)
read_only=1

## 让超级管理员也只读(防止代码用 root 账号连从库时误写数据)
super_read_only=1

重启slave实例:

1
2
$ docker restart mysql02
$ docker ps

在从机上配置需要同步的主机服务器信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 在 master 实例节点,执行查看主库状态
$$$ SHOW BINARY LOG STATUS;
+-----------------------+----------+--------------+------------------+-------------------+
| File | Position | Binlog_Do_DB | Binlog_Ignore_DB | Executed_Gtid_Set |
+-----------------------+----------+--------------+------------------+-------------------+
| mall-mysql-bin.000001 | 158 | | mysql | |
+-----------------------+----------+--------------+------------------+-------------------+

# 在 slave 实例节点,执行 “拜码头” 命令
$ docker exec -it 69e24e18efe2 mysql -uroot -p123456

$$$ CHANGE REPLICATION SOURCE TO SOURCE_HOST='192.168.1.8', SOURCE_USER='slave', SOURCE_PASSWORD='123456', SOURCE_PORT=3306, SOURCE_LOG_FILE='mall-mysql-bin.000001', SOURCE_LOG_POS=158, SOURCE_CONNECT_RETRY=30;

# 查看同步状态
$$$ SHOW REPLICA STATUS\G;
*************************** 1. row ***************************
Replica_IO_State:
Source_Host: 192.168.1.8
Source_User: slave
Source_Port: 3306
Connect_Retry: 30
Source_Log_File: mall-mysql-bin.000001
Read_Source_Log_Pos: 158
Relay_Log_File: mall-mysql-relay-bin.000001
Relay_Log_Pos: 4
Relay_Source_Log_File: mall-mysql-bin.000001
Replica_IO_Running: No ####
Replica_SQL_Running: No ####
...

# 在从数据库中开启同步进程
$$$ START REPLICA;
$$$ SHOW REPLICA STATUS\G
*************************** 1. row ***************************
Replica_IO_State:
Source_Host: 192.168.1.8
Source_User: slave
Source_Port: 3306
Connect_Retry: 30
Source_Log_File: mall-mysql-bin.000001
Read_Source_Log_Pos: 158
Relay_Log_File: mall-mysql-relay-bin.000001
Relay_Log_Pos: 4
Relay_Source_Log_File: mall-mysql-bin.000001
Replica_IO_Running: No
Replica_SQL_Running: Yes
...
Last_IO_Error: Error connecting to source 'slave@192.168.1.8:3306'. This was attempt 10/10, with a delay of 30 seconds between attempts. Message: Authentication plugin 'caching_sha2_password' reported error: Authentication requires secure connection.
...

# 在 MySQL 8.4 中,从库(Slave)尝试通过 slave 账号去连主库。因为主库上的 slave 用户使用的是新版 caching_sha2_password 加密
# 这种加密方式默认强制要求要么走加密的 SSL 通道,要么必须在握手时主动去主库获取公钥(RSA Public Key)。
## 解决这个问题最快、最优雅的办法,是在从库上执行一个“追加配置参数”命令。告诉从库允许在连接主库时,主动去索取公钥用于密码解密。
### 在从数据库,先停掉当前的同步进程(解绑状态)
$$$ STOP REPLICA;
### 在从数据库,追加核心解锁参数:GET_SOURCE_PUBLIC_KEY=1
$$$ CHANGE REPLICATION SOURCE TO GET_SOURCE_PUBLIC_KEY=1;
### 在从数据库,重新开启同步进程
$$$ START REPLICA;

### 在从服务器上配置允许的远程连接
$$$ SET GLOBAL super_read_only = 0; ## 临时允许从库超级管理员可修改
$$$ ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
$$$ FLUSH PRIVILEGES;
$$$ SET GLOBAL super_read_only = 1; ## 重新设置从库超级管理员只读

主从复制测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 在master上创建数据库
$$$ create database db03;
# 在slave上能够马上看到新创建的数据库
$$$ show databases;

# 在master上创建表
$$$ create table t_user(id int not null primary key auto_increment, name varchar(20));
# 在slave上能够马上看到新创建的表
$$$ show tables;

# 在master上新增、删除、更新数据记录
$$$ insert t_user(id,name) values (2, 'zhangsan002');
# 在slave上能够马上看到新增、删除、更新的记录(在从库)
$$$ select * from t_user;


Redis

单机版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 查询和拉取镜像
$ docker pull redis:8.8.0


# 启动一个 redis 容器实例
# 在执行 docker run -v /宿主机路径:/容器内路径 时,Docker 有一套铁律:
# 如果宿主机路径下的东西已经存在(无论是文件还是文件夹),Docker 会老老实实进行精准挂载。
# 如果宿主机路径下的东西“不存在”:Docker 无法预知你到底是想要一个文件还是一个文件夹。
# 由于 Linux 下一切皆文件,Docker 统一采取默认保守策略——直接在宿主机上帮你递归创建成一个全新的文件夹
# 因此,必须确保 /opt/apps/redis/conf/redis.conf 事先存在,这样在挂载数据卷才能精准映射文件而不是文件夹
$ mkdir -p /opt/apps/redis/conf
$ touch /opt/apps/redis/conf/redis.conf
# 往配置文件里写入核心生产配置
$ cat <<EOF > /opt/apps/redis/conf/redis.conf
# 允许任意 IP 连接访问
bind 0.0.0.0
# 非守护模式启动
daemonize no
# 关闭保护模式,允许外部网络访问
protected-mode no
# 开启 AOF 持久化(强力数据保护)
appendonly yes
# 设置你的强 Redis 访问密码
requirepass 123456
EOF

$ docker run -d \
-p 6379:6379 \
--name=redis01 \
-v /opt/apps/redis/conf/redis.conf:/etc/redis/redis.conf \
-v /opt/apps/redis/data:/data \
redis:8.8.0 \
redis-server /etc/redis/redis.conf \
#--restart=unless-stopped \ 重启策略:保证服务器断电重启后,Redis 能自动重新在后台拉起。


$$ redis-cli
$$ auth 123456
$$ set a 111
$$ get a
"111"


集群版

哈希槽理论部分

下面通过哈希取余算法、一致性哈希算法、哈希槽算法的演进史,介绍实际生产中经常遇到的分布式系统分布式寻址、高可用、扩容缩容等核心问题。

  • 哈希取余算法(Hash Modulo)—— 基础传统方案
    • 核心原理:假设系统有 $N$ 台机器,当一个请求/数据进来时,先计算其 Key 的哈希值,然后直接对机器数量 $N$ 取余,通过计算结果,数据被直接路由到对应的机器上。
    • 优点:简单粗暴,计算速度极快,数据在节点数量固定的情况下分布相对均匀。
    • 致命痛点(扩缩容雪崩):一旦节点数量 $N$ 发生变化(增加或减少一台机器),所有数据的路由公式全变了!比如 $N$ 从 3 变成 4,原本 $\pmod 3$ 的数据大面积失效,导致旧数据几乎需要 100% 重新迁移(Rehash)。在缓存场景下,这会导致缓存大面积瞬间失效,引发缓存雪崩,流量直接冲垮后端数据库。
  • 一致性哈希算法(Consistent Hashing)—— 革命性的圆环设计
    • 核心原理:一致性哈希不再直接对节点数取余,而是引入了一个固定的 哈希环(大小为 $2^{32}$)。
      • 节点映射:将每台服务器的 IP 或主机名进行哈希,计算出的结果对应到环上的某个位置。
      • 数据映射:当数据 Key 进来时,同样计算哈希值映射到环上。
      • 路由规则:从数据落点位置开始,顺时针寻找碰到的第一台服务器,该服务器即为数据的存储节点。
    • 如何解决痛点(扩缩容优势):当新增或删除一个节点时,受影响的只有该节点在环上逆时针方向到前一个节点之间的一小段数据,其余绝大部分数据依然顺时针路由到原本的节点。数据迁移量从“全量”降到了“局部($\frac{1}{N}$)”。
    • 哈希倾斜与虚拟节点(Virtual Nodes):
      • 问题:如果服务器节点太少,它们在环上的分布可能不均匀,导致某些节点承载极其恐怖的流量(数据倾斜)。
      • 解法:引入虚拟节点。把一台真实的物理机虚拟成几百个节点(如 NodeA-1, NodeA-2)散落在环上。虚拟节点越多,数据分布就越趋近于绝对均匀。
  • 哈希槽算法(Hash Slots)—— 现代工业级落地(以 Redis Cluster 为代表)
    • 核心原理:哈希槽(以 Redis 为例)将整个集群抽象地划分为固定数量的槽位(16384 个槽,即 0 - 16383)。
      • 槽位分配:集群启动时,把这 16384 个槽平均或按性能手动分配给不同的物理节点(如 NodeA 负责 0-5000,NodeB 负责 5001-10000…)。
      • 数据寻址:数据 Key 进来时,利用 CRC16 算法计算出哈希值,然后对固定的 16384 取余,得到一个槽号:$\text{Slot Index} = \text{CRC16(Key)} \pmod{16384}$
      • 根据槽号,直接去找负责这个槽的服务器。
    • 为什么可以替代一致性哈希?:
      • 解耦了数据与节点的直接映射:在一致性哈希中,数据路由完全取决于节点在环上的物理位置,节点变动时,数据的迁移是伴随着底层哈希指针的改变被动发生的。 而哈希槽算法中,数据只和槽绑定,槽和节点绑定。
      • 完美的精细化扩缩容控制(核心):当你想增加一台新机器时,你可以精准地从原本的机器里各自抽出一部分槽(比如各抽 1000 个槽)挪给新机器。这种槽的移动是完全由运维人员或自动化脚本 “人工控制” 的。在迁移过程中,Redis 还可以继续对外提供读写服务(通过 Ask/Moved 重定向机制),实现了真正意义上的平滑扩容,而不需要像一致性哈希那样去重新计算复杂的环上顺时针跨度。
      • 一目了然的集群状态:哪个节点负责哪些数据,清清楚楚。如果某台机器挂了,只需要把她负责的槽对应的从库(Replica)提为主库,或者把槽转移走即可,极易维护。
    • 为什么 redis 只有 16384 个槽?
      • Redis 的作者 Antirez 曾经在 GitHub 上亲自回答过这个问题。总结起来,选择*16384(即 16K,即 $2^{14}$) 而不是更直观的 65536(即 64K,即 $2^{16}$),完全是在网络心跳开销、集群规模上限以及压缩效率之间做出的顶级工程权衡(Trade-off)。
      • 具体原因之一:心跳包(Ping/Pong)的带宽开销太大。Redis Cluster 中的每个节点都需要定期向其他节点发送 PING 消息,以便检测对方是否在线。为了让其他节点知道 “我这个节点目前负责哪些槽”,PING 消息的报文头(Header)里必须附带一个位图(Bitmap),用每一位(bit)来代表自己是否负责对应的槽。如果是 16384 个槽:位图大小为:$16384 \div 8 \text{ bit} = 2048 \text{ 字节} = \mathbf{2\text{ KB}}$。如果是 65536 个槽:位图大小为:$65536 \div 8 \text{ bit} = 8192 \text{ 字节} = \mathbf{8\text{ KB}}$。在 Redis 集群中,心跳是非常频繁的(每秒都会有很多 PING/PONG 发送)。如果一个拥有上百个节点的集群,每次心跳仅槽位信息就要额外多吃掉 6 KB 的带宽,整个集群的内网网络带宽会被这些无意义的心跳报文严重榨干。因此,作者为了极致的吞吐量,必须精简心跳包的大小。
      • 具体原因之二:集群规模的现实制约——节点数不可能超过 1000 个。Redis 作者认为,Redis Cluster 的集群规模绝对不建议超过 1000 个 Master 节点。如果节点数超过 1000 个,节点间八卦协议(Gossip)的网络广播就会引发“网络风暴”,集群内部自己就把自己卡死了。在节点数不超过 1000 个的前提下,16384 个槽已经绰绰有余了——即使有 1000 个节点,平均每个节点也能分到 16 个槽,完全能够满足极细粒度的数据分片和动态扩容需求。所以,盲目把槽扩大到 65536 没有任何实际工程意义。
      • 具体原因之三:底层压缩神技——16384 在特定场景下压缩率极高。Redis 在传输槽位图时,并不是一成不变的,在很多时候会对位图进行压缩后再传输。当槽数量为 16384 时:由于集群节点数一般比较少,每个节点分到的槽通常是连续且成块的(比如 NodeA 负责 0-5000)。这种高连续性的位图,在使用位图压缩算法时,其压缩比恐怖到惊人(一堆连续的 1 可以被极度压缩),2 KB 的数据常常能被压缩到几十个字节。如果强行换成 65536 个槽,由于槽位的基数变大,在分片、迁移等过程中,位图中的零散度会成倍上升,导致压缩率大打折扣。

总结起来就是:哈希取余属于早期的静态分片方案,简单但没有弹性,不具备动态扩缩容能力; 一致性哈希通过 ‘哈希环’ 巧妙地解决了扩容时数据全量失效的痛点,但在工程落地时,其被动的顺时针路由导致扩容粒度较粗、不易精细化控制; 哈希槽算法则是现代分布式存储(如 Redis Cluster)的工业级标准,它引入了 ‘虚拟槽’ 这一中间层,彻底解耦了数据与节点的直接映射,让大规模集群的平滑扩容、在线数据迁移变得极其可控与稳定。


三主三从集群的搭建

第一步:一键创建宿主机目录与配置文件。为了不重蹈 “挂载成文件夹” 的覆辙,我们直接采用挂载整个配置目录的高级规范。同时,由于有 6 个节点,手写 6 个配置文件太低效了。在宿主机终端直接复制并运行以下命令,它会自动帮你格式化创建出 6 个节点所需的文件夹和 redis.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 1. 创建专属的 Docker 自定义网络,命名为 redis-net
$ docker network --help
$ docker network create redis-net
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
2133c9ab0010 bridge bridge local
d3385e8df5e5 host host local
c788000c6843 none null local
b165ac73c636 redis-net bridge local


# 2. 批量创建 6 个节点的目录,并写入集群配置
$ for port in $(seq 6381 6386); do \
mkdir -p /opt/apps/redis-cluster/${port}/conf /opt/apps/redis-cluster/${port}/data; \

cat <<EOF > /opt/apps/redis-cluster/${port}/conf/redis.conf
# 业务端口
port ${port}
# 允许任意 IP 访问
bind 0.0.0.0
# 关闭保护模式
protected-mode no
# 容器环境必须为 no
daemonize no
# 开启 AOF 持久化
appendonly yes
# 设置密码
requirepass 123456
# 集群节点间访问也需要密码
masterauth 123456

# === 核心集群配置 ===
# 开启集群模式
cluster-enabled yes
# 集群配置文件名称(容器会自动创建并维护)
cluster-config-file nodes.conf
# 节点超时时间(毫秒)
cluster-node-timeout 5000
# 宣告宿主机的物理 IP(关键!防止外界无法路由)
cluster-announce-ip 192.168.1.8
# 宣告业务端口
cluster-announce-port ${port}
# 宣告集群总线端口(业务端口 + 10000)
cluster-announce-bus-port 1${port}
EOF
done

第二步:一键启动 6 个 Redis 容器节点。目录和配置都生成好了,接下来我们用一个循环,把 6 个容器齐刷刷地全部拉起来。在宿主机终端继续执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ for port in $(seq 6381 6386); do \
docker run -d \
--name redis-${port} \
--net redis-net \
-p ${port}:${port} \
-p 1${port}:1${port} \
--privileged=true \
-v /opt/apps/redis-cluster/${port}/conf:/etc/redis \
-v /opt/apps/redis-cluster/${port}/data:/data \
--restart=unless-stopped \
redis:8.8.0 \
redis-server /etc/redis/redis.conf; \
done

$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
008a099e2434 redis:8.8.0 "docker-entrypoint.s…" 13 seconds ago Up 11 seconds 0.0.0.0:6386->6386/tcp, [::]:6386->6386/tcp, 6379/tcp, 0.0.0.0:16386->16386/tcp, [::]:16386->16386/tcp redis-6386
85f69e3bf668 redis:8.8.0 "docker-entrypoint.s…" 13 seconds ago Up 12 seconds 0.0.0.0:6385->6385/tcp, [::]:6385->6385/tcp, 6379/tcp, 0.0.0.0:16385->16385/tcp, [::]:16385->16385/tcp redis-6385
9f382490b3ca redis:8.8.0 "docker-entrypoint.s…" 14 seconds ago Up 13 seconds 0.0.0.0:6384->6384/tcp, [::]:6384->6384/tcp, 6379/tcp, 0.0.0.0:16384->16384/tcp, [::]:16384->16384/tcp redis-6384
9079ded11550 redis:8.8.0 "docker-entrypoint.s…" 15 seconds ago Up 13 seconds 0.0.0.0:6383->6383/tcp, [::]:6383->6383/tcp, 6379/tcp, 0.0.0.0:16383->16383/tcp, [::]:16383->16383/tcp redis-6383
a5478222791f redis:8.8.0 "docker-entrypoint.s…" 15 seconds ago Up 14 seconds 0.0.0.0:6382->6382/tcp, [::]:6382->6382/tcp, 6379/tcp, 0.0.0.0:16382->16382/tcp, [::]:16382->16382/tcp redis-6382
05e4dabe8995 redis:8.8.0 "docker-entrypoint.s…" 16 seconds ago Up 15 seconds 0.0.0.0:6381->6381/tcp, [::]:6381->6381/tcp, 6379/tcp, 0.0.0.0:16381->16381/tcp, [::]:16381->16381/tcp redis-6381

第三步:握手组建集群(分配 3主3从)。现在 6 个节点只是孤独运行的单机,我们需要把它们召集起来,指定 3主3从,并让它们自动瓜分 16384 个哈希槽。由于我们配置了密码,所以在创建集群时,必须带上 -a 参数。随便挑一个容器(比如连入 redis-6381)去执行集群初始化命令。直接复制运行下面这行:

1
2
3
4
5
6
# --cluster-replicas 1:代表为每个 Master 指定 1 个 Replica(从库)
# 算法会自动把前 3 个 IP 自动提升为 Master,把后 3 个 IP 自动分配为对应的 Slave,并且交叉跨机器绑定,保证高可用。
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster create \
192.168.1.8:6381 192.168.1.8:6382 192.168.1.8:6383 \
192.168.1.8:6384 192.168.1.8:6385 192.168.1.8:6386 \
--cluster-replicas 1

第四步:集群验证。集群和普通单机不一样,测试时需要加上 -c 参数(代表集群模式,开启自动槽位重定向)。我们可以进入 6381 节点塞入一个数据,看看它会不会被自动路由到其他 Master 节点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# 连入集群(注意带上 -c)
$ docker exec -it redis-6381 redis-cli -c -a 123456 -p 6381

# 写入一条数据
127.0.0.1:6381> set k1 "v1"
-> Redirected to slot [12706] located at 192.168.1.8:6383 ### CRC16(mykey) % 16384
OK

# 查看集群信息
192.168.1.8:6383> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_ping_sent:444
cluster_stats_messages_pong_sent:431
cluster_stats_messages_meet_sent:1
cluster_stats_messages_sent:876
cluster_stats_messages_ping_received:431
cluster_stats_messages_pong_received:448
cluster_stats_messages_received:879
total_cluster_links_buffer_limit_exceeded:0
cluster_slot_migration_active_tasks:0
cluster_slot_migration_active_trim_running:0
cluster_slot_migration_active_trim_current_job_keys:0
cluster_slot_migration_active_trim_current_job_trimmed:0
cluster_slot_migration_stats_active_trim_started:0
cluster_slot_migration_stats_active_trim_completed:0
cluster_slot_migration_stats_active_trim_cancelled:0

# 查看集群节点
192.168.1.8:6383> cluster nodes
424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780196534000 2 connected 5461-10922
6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 master - 0 1780196534868 1 connected 0-5460
8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 myself,master - 0 0 3 connected 10923-16383
aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780196535000 3 connected
cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 slave 6c37172dde90d68cde4605cb25733072c708ed3e 0 1780196534000 1 connected
5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780196535874 2 connected

# 集群检查命令
$$ redis-cli --cluster check 192.168.1.8:6381 -a 123456
192.168.1.8:6381 (6c37172d...) -> 0 keys | 5461 slots | 1 slaves.
192.168.1.8:6382 (424a976b...) -> 0 keys | 5462 slots | 1 slaves.
192.168.1.8:6383 (8ec9e604...) -> 1 keys | 5461 slots | 1 slaves.
[OK] 1 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 192.168.1.8:6381)
M: 6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381
slots:[0-5460] (5461 slots) master
1 additional replica(s)
M: 424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382
slots:[5461-10922] (5462 slots) master
1 additional replica(s)
M: 8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383
slots:[10923-16383] (5461 slots) master
1 additional replica(s)
S: 5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386
slots: (0 slots) slave
replicates 424a976bfbf96a0bdae0012157449ea6907f9d17
S: cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385
slots: (0 slots) slave
replicates 6c37172dde90d68cde4605cb25733072c708ed3e
S: aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384
slots: (0 slots) slave
replicates 8ec9e60429b52db4a982502839c8a52c01015905
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

目前节点的状态图:

1
2
3
master 6381 {0-5460}       --------  slave 6385
master 6382 {5461-10922} -------- slave 6386
master 6383 {10923-16383} -------- slave 6384


主从容错的演示

第一:验证停掉 master 6381,看看 slave 6385 是否会升级为新的 master。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 停  master 6381
$ docker ps
$ docker stop redis-6381

# 连接任意一个节点
$ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382

# 查看集群节点,发现 slave 6385 ---> master 6385
127.0.0.1:6382> cluster nodes
8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780197842111 3 connected 10923-16383
424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 myself,master - 0 0 2 connected 5461-10922
5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780197843126 2 connected
6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 master,fail - 1780197660355 1780197657783 1 disconnected
aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780197842000 3 connected
cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780197841604 7 connected 0-5460

# 此时节点的状态图:
master 6385 {0-5460} -------- x
master 6382 {5461-10922} -------- slave 6386
master 6383 {10923-16383} -------- slave 6384

第二:验证重新启动 6381,看看新集群的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ docker start redis-6381

# 连接任意一个节点
$ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382

# 查看新的集群节点,发现 6381 变成了 6385 的从节点
127.0.0.1:6382> cluster nodes
8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780198267509 3 connected 10923-16383
424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 myself,master - 0 0 2 connected 5461-10922
5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780198267509 2 connected
6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 1780198267812 7 connected
aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780198266601 3 connected
cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780198266803 7 connected 0-5460

# 此时节点的状态图:
master 6385 {0-5460} -------- slave 6381
master 6382 {5461-10922} -------- slave 6386
master 6383 {10923-16383} -------- slave 6384


集群扩容演示

在原6台节点集群的基础上,增加 redis-6387、redis-6388 两个节点,redis-6387 作为新的主节点(获取槽位),redis-6388 作为 redis-6387 的从节点。

第一步:在宿主机创建新节点目录并启动容器。首先,我们需要在宿主机(192.168.1.8)上把 6387 和 6388 的家当准备好,并把它们拉起来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 1. 批量创建 6387 和 6388 的目录并写入集群配置,然后启动新容器,加入原有的 redis-net 网络
$ for port in 6387 6388; do \
mkdir -p /opt/apps/redis-cluster/${port}/conf /opt/apps/redis-cluster/${port}/data; \

cat <<EOF > /opt/apps/redis-cluster/${port}/conf/redis.conf
port ${port}
bind 0.0.0.0
protected-mode no
daemonize no
appendonly yes
requirepass 123456
masterauth 123456
cluster-enabled yes
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 192.168.1.8
cluster-announce-port ${port}
cluster-announce-bus-port 1${port}
EOF

docker run -d \
--name redis-${port} \
--net redis-net \
-p ${port}:${port} \
-p 1${port}:1${port} \
--privileged=true \
-v /opt/apps/redis-cluster/${port}/conf:/etc/redis \
-v /opt/apps/redis-cluster/${port}/data:/data \
--restart=unless-stopped \
redis:8.8.0 \
redis-server /etc/redis/redis.conf; \
done

第二步:将新节点作为无槽空节点加入集群。新容器启动后,它们还只是孤立的单机,我们需要使用 redis-cli –cluster add-node 命令把它们引荐给集群。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 将 6387 作为 Master 角色加入集群。add-node 新节点IP:端口 已存在节点IP:端口
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster add-node 192.168.1.8:6387 192.168.1.8:6381

# 将 6388 同样先作为普通节点加入集群
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster add-node 192.168.1.8:6388 192.168.1.8:6381 --cluster-slave --cluster-master-id cb71175feac4f019edac69031c45e42d63c3e197

## 如果6388先辈加成了主节点,需要事后纠正:告诉 6388,立刻放弃当前无槽 Master 的身份,去当 6387 的复制品
## docker exec -it redis-6388 redis-cli -p 6388 -a 123456
## 127.0.0.1:6388> CLUSTER REPLICATE cb71175feac4f019edac69031c45e42d63c3e197

# 连接任意一个节点,查看集群节点状态
$ docker exec -it redis-6382 redis-cli -c -a 123456 -p 6382
127.0.0.1:6382> cluster nodes

# 发现此时节点的状态图:
master 6385 {0-5460} -------- slave 6381
master 6382 {5461-10922} -------- slave 6386
master 6383 {10923-16383} -------- slave 6384
master 6387 {} -------- slave 6388

第三步:在线重新分配哈希槽(Reshard)给 6387。们现在要把原本 3 个老 Master 治下的 16384 个槽,匀出一部分给 6387。原本 3 主时代,平均每个主负责 $16384 \div 3 = 5461$ 个槽。现在变为了 4 主,平均每个主应该负责 $16384 \div 4 = \mathbf{4096}$ 个槽。也就是说,新主节点 6387 应该从老的三台主节点身上一共抽调 4096 个槽过来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 获取 6387 的节点 ID(Node ID)
$ docker exec -it redis-6381 redis-cli -a 123456 -p 6381 cluster nodes | grep 6387

# 启动交互式分槽向导
## 询问:How many slots do you want to move (from 1 to 16384)?
## 👉 输入:4096 (代表我们要分给新店 4096 个槽)
## 询问:What is the receiving node ID?
## 👉 粘贴:刚才复制的 6387 的 Node ID (代表这些槽全部给 6387)
## 询问:Please enter all the source node IDs.
## 👉 输入:all (代表从目前所有老的主节点里,平均按比例抽取这 4096 个槽)
## 询问:Do you want to proceed with the proposed reshard plan (yes/no)?
## 👉 输入:yes 见证奇迹的时刻:Redis 会开始疯狂刷屏,把槽位连同槽位里原本存在的数据实时、平滑地迁移到 6387 上。期间 Java 业务网关可以继续正常读写,完全不受影响!
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster reshard 192.168.1.8:6381

# 连接任意一个节点,查看集群节点状态
$ docker exec -it redis-6381 redis-cli -c -a 123456 -p 6381
127.0.0.1:6381> cluster nodes
424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780201520553 2 connected 6827-10922
6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 1780201520000 7 connected
8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 myself,master - 0 0 3 connected 12288-16383
65600984ab36afd9a4106cd6c668547e9e5fa75f 192.168.1.8:6388@16388 slave cb71175feac4f019edac69031c45e42d63c3e197 0 1780201518710 9 connected
aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780201520552 3 connected
cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780201518508 7 connected 1365-5460
5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780201520956 2 connected
cb71175feac4f019edac69031c45e42d63c3e197 192.168.1.8:6387@16387 master - 0 1780201520000 9 connected 0-1364 5461-6826 10923-12287

# 发现此时节点的状态图:
master 6385 {1365-5460} -------- slave 6381
master 6382 {6827-10922} -------- slave 6386
master 6383 {12288-16383} -------- slave 6384
master 6387 {0-1364 5461-6826 10923-12287} -------- slave 6388


集群缩容演示

现在集群的压力没有这么大了,需要缩容,删除 redis-6387 和 redis-6388 节点。将多出的槽位均分到之前的节点上。

第一步:把 6387 上的槽位吐出来。先把 6387 的槽一次性全倒给 6385【接收槽位的必须是主节点】。此时,6387 瞬间沦为空壳(0槽),它已经达到了可以被安全删除的标准。而 6385 此时暴涨到了 $4096 + 4096 = 8192$ 个槽,处于严重的“消化不良”状态。

1
2
3
4
5
6
7
8
9
10
11
# How many slots do you want to move (from 1 to 16384)?
# 👉 输入:4096 (全搬走)
# What is the receiving node ID?
# 👉 输入 6385 的 Node ID:cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9
# Please enter all the source node IDs.
# 👉 输入 6387 的 Node ID:cb71175feac4f019edac69031c45e42d63c3e197
# Source node IDs...done?
# 👉 输入:done
# Do you want to proceed with the proposed reshard plan (yes/no)?
# 👉 输入:yes
$ docker exec -it redis-6385 redis-cli -a 123456 --cluster reshard 192.168.1.8:6385

第二步:召唤一键平衡神技(Rebalance)

现在 6387 已经空了,我们先把 6387 和 6388 删掉(避免它们参与后续的槽位瓜分)。

1
2
3
4
5
# 删从节点 6388
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster del-node 192.168.1.8:6381 65600984ab36afd9a4106cd6c668547e9e5fa75f

# 删主节点 6387
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster del-node 192.168.1.8:6381 cb71175feac4f019edac69031c45e42d63c3e197

执行自动均衡命令。此时集群里只剩下原本的老三台 Master 和老三台 Slave了。我们不需要再苦哈哈地去算每台机器分多少,直接掏出 Redis Cluster 的大杀器——rebalance:Redis 收到命令后,会扫视当前集群中所有健康的 Master 节点。它发现 6385 一个人胖成了球,而 6382 和 6383 瘦得可怜(默认的倾斜程度阈值为2%)。于是算法在后台会自动、精准地将 6385 身上多出来的槽位均分给 6382 和 6383,直到大家的槽位重新恢复成完美的 5461 或 5462(16384/3)。

1
2
3
4
5
6
7
8
9
10
11
# --cluster-threshold 1:告诉 Redis,节点之间的数据倾斜到什么程度时,才真正触发“自动重新分配槽位”的操作。其中的数字 1 代表 1%。
$ docker exec -it redis-6381 redis-cli -a 123456 --cluster rebalance 192.168.1.8:6381 --cluster-threshold 1

# 查看节点的状态
127.0.0.1:6381> cluster nodes
aafd2fe35dcba6f53e304ca4ef19a8efc8d8e139 192.168.1.8:6384@16384 slave 8ec9e60429b52db4a982502839c8a52c01015905 0 1780204082000 11 connected
8ec9e60429b52db4a982502839c8a52c01015905 192.168.1.8:6383@16383 master - 0 1780204083000 11 connected 0-2731 13654-16383
5cfd7c6bda4db51ee16ce2734027195c5f24edb7 192.168.1.8:6386@16386 slave 424a976bfbf96a0bdae0012157449ea6907f9d17 0 1780204082000 12 connected
424a976bfbf96a0bdae0012157449ea6907f9d17 192.168.1.8:6382@16382 master - 0 1780204083575 12 connected 2732-5461 8192-10922
cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 192.168.1.8:6385@16385 master - 0 1780204083979 10 connected 5462-8191 10923-13653
6c37172dde90d68cde4605cb25733072c708ed3e 192.168.1.8:6381@16381 myself,slave cbfb450db0e9e3bf0b33f41244d24c7b7ed7ace9 0 0 10 connected

在实际生产环境中,由于肉眼计算和手动复制 40 位的 Node ID 极易出错(比如不小心拷错了一个字母,或者数字算错了一位导致槽位没对齐),手动一次性迁移 + 官方 rebalance 自动化均分的组合拳,能够将人为操作失误的概率降到最低。